package gpstools.data;

import gpstools.Point;
import gpstools.track.LapInfo;
import gpstools.track.Track;
import gpstools.track.TrackInfo;
import gpstools.track.TrackPoint;
import gpstools.track.TrackPoint.Builder;

import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;
import java.util.Queue;

import com.garmin.fit.Decode;
import com.garmin.fit.LapMesg;
import com.garmin.fit.LapMesgListener;
import com.garmin.fit.MesgBroadcaster;
import com.garmin.fit.RecordMesg;
import com.garmin.fit.RecordMesgListener;
import com.garmin.fit.SessionMesg;
import com.garmin.fit.SessionMesgListener;

/**
 * 
 * @author spr
 * 
 */
public class FitLoader implements Loader, TrackSource {
    private static final String EXTENSION = "fit";
    private Queue<Track> tracks = new LinkedList<Track>();

    FitLoader() {
    }
    
    private final static double SEMICIRCLE_FACTOR = (double) (1L << 31) / 180.0d;

    private static double convertSemicirclesToDegrees(int semicircle) {
        return ((double) semicircle / SEMICIRCLE_FACTOR);
    }

    private static TrackPoint processRecord(RecordMesg recordMesg) {
        /* standard, required */
        if (recordMesg.getPositionLat() == null || recordMesg.getPositionLong() == null
                || recordMesg.getTimestamp() == null || recordMesg.getAltitude() == null) {
            return null;
        }

        final double lon = convertSemicirclesToDegrees(recordMesg.getPositionLong());
        final double lat = convertSemicirclesToDegrees(recordMesg.getPositionLat());
        final double ele = recordMesg.getAltitude();
        final long time = recordMesg.getTimestamp().getDate().getTime();

        Builder builder = TrackPoint.builder(new Point(lat, lon, ele)).time(time);

        if (recordMesg.getHeartRate() != null) {
            builder.heartRate(recordMesg.getHeartRate());
        }
        if (recordMesg.getCadence() != null) {
            builder.cadence(recordMesg.getCadence());
        }
        if (recordMesg.getPower() != null) {
            builder.power(recordMesg.getPower());
        }        
        
        return builder.build();
    }

    private Track.Builder builder = Track.builder();

    // List<Lap> laps = new ArrayList<Lap>();
    //
    // List<TrackPoint> points = new ArrayList<TrackPoint>();

    public FitLoader(byte[] data) throws UnsupportedFormatException {
        this(new ByteArrayInputStream(data));
    }

    public FitLoader(InputStream inputStream) throws UnsupportedFormatException {

        MesgBroadcaster broadcaster = new MesgBroadcaster(new Decode());
        broadcaster.addListener(new RecordMesgListener() {

            @Override
            public void onMesg(RecordMesg recordMesg) {
                addPoint(recordMesg);
            }
        });
        broadcaster.addListener(new LapMesgListener() {

            @Override
            public void onMesg(LapMesg lapMesg) {
                closeLap(lapMesg);
            }
        });

        broadcaster.addListener(new SessionMesgListener() {

            @Override
            public void onMesg(SessionMesg sessionMsg) {

                closeLap(null);
                if (!builder.isEmpty()) {
                    Track track = builder.build();
                    TrackInfo info = track.getInfo();
                    if (sessionMsg.getTotalDistance() != null) {
                        info.setDistance(sessionMsg.getTotalDistance().doubleValue());
                    }
                    if (sessionMsg.getTotalDistance() != null) {
                        info.setDistance(sessionMsg.getTotalDistance().doubleValue());
                    }
                    if (sessionMsg.getTotalAscent() != null) {
                        info.setElevationGain(sessionMsg.getTotalAscent().doubleValue());
                    }
                    if (sessionMsg.getTotalDescent() != null) {
                        info.setElevationLoss(sessionMsg.getTotalDescent().doubleValue());
                    }
                    info.setAverageHeartRate(sessionMsg.getAvgHeartRate());
                    info.setAverageCadence(sessionMsg.getAvgCadence());
                    if (sessionMsg.getAvgSpeed() != null) {
                        info.setAverageSpeed(sessionMsg.getAvgSpeed().doubleValue());
                    }
                    // sessionMsg.getTotalElapsedTime() // TODO ??
                    // sessionMsg.getTotalTimerTime() // TODO ??
                    
                    tracks.add(track);
                }
            }
        });
        broadcaster.run(inputStream);
    }

    public FitLoader(String fileName) throws FileNotFoundException, UnsupportedFormatException {
        this(new FileInputStream(fileName));
    }

    private void addPoint(RecordMesg recordMesg) {
        TrackPoint point = processRecord(recordMesg);
        builder.add(point);
    }

    private void closeLap(LapMesg lapMesg) {
        LapInfo info = builder.closeLap();
        if (lapMesg != null) {
            if (lapMesg.getTotalDistance() != null) {
                info.setDistance(lapMesg.getTotalDistance().doubleValue());
            }
            if (lapMesg.getTotalAscent() != null) {
                info.setElevationGain(lapMesg.getTotalAscent().doubleValue());
            }
            if (lapMesg.getTotalDescent() != null) {
                info.setElevationLoss(lapMesg.getTotalDescent().doubleValue());
            }
            info.setAverageHeartRate(lapMesg.getAvgHeartRate());
            info.setAverageCadence(lapMesg.getAvgCadence());
            if (lapMesg.getAvgSpeed() != null) {
                info.setAverageSpeed(lapMesg.getAvgSpeed().doubleValue());
            }
            // lapMesg.getTotalElapsedTime() // TODO ??
            // lapMesg.getTotalTimerTime() // TODO ??
        }
    }

    @Override
    public Track next() {
        return tracks.poll();
    }

    @Override
    public TrackSource load(String fileName) throws IOException, UnsupportedFormatException {
        return new FitLoader(fileName);
    }
    
    @Override
    public TrackSource load(byte[] data) throws UnsupportedFormatException {
        return new FitLoader(data);
    }

    @Override
    public String getExtension() {
        return EXTENSION;
    }
}
